package it.uniroma2.art.coda.pearl.parser;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import it.uniroma2.art.coda.exception.PRParserException;
import it.uniroma2.art.coda.interfaces.ParserPR;
import it.uniroma2.art.coda.pearl.model.BindingStruct;
import it.uniroma2.art.coda.pearl.model.GraphElement;
import it.uniroma2.art.coda.pearl.model.GraphStruct;
import it.uniroma2.art.coda.pearl.model.OptionalGraphStruct;
import it.uniroma2.art.coda.pearl.model.PlaceholderStruct;
import it.uniroma2.art.coda.pearl.model.ProjectionRule;
import it.uniroma2.art.coda.pearl.model.ProjectionRulesModel;
import it.uniroma2.art.coda.pearl.model.graph.GraphSingleElemBNode;
import it.uniroma2.art.coda.pearl.model.graph.GraphSingleElemPlaceholder;
import it.uniroma2.art.coda.pearl.model.graph.GraphSingleElemUri;
import it.uniroma2.art.coda.pearl.model.graph.GraphSingleElemVar;
import it.uniroma2.art.coda.pearl.model.graph.GraphSingleElement;
import it.uniroma2.art.coda.pearl.parser.antlr.AntlrLexer;
import it.uniroma2.art.coda.pearl.parser.antlr.AntlrParser;

import org.antlr.runtime.ANTLRFileStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.TokenStream;
import org.antlr.runtime.tree.Tree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PearlParser extends ParserPR {

	private static final String DEFAULT_CONVERTER_URI = "http://art.uniroma2.it/coda/converters/default";

	public PearlParser(String id, String description) {
		super(id, description);
	}

	private static Logger logger = LoggerFactory.getLogger(PearlParser.class);

	@Override
	public ProjectionRulesModel parsePearlDocument(File prFile) throws PRParserException {
		try {
			CharStream charStream = new ANTLRFileStream(prFile.getPath());
			AntlrLexer lexer = new AntlrLexer(charStream);
			TokenStream token = new CommonTokenStream(lexer);
			AntlrParser parser = new AntlrParser(token);

			Tree pearlUnit = (Tree) parser.pearlUnit().getTree();
			return parsePearlUnit(pearlUnit);
		} catch (Exception e) {
			throw new PRParserException(e);
		}
	}

	private ProjectionRulesModel parsePearlUnit(Tree pearlUnit) {
		logger.info("pearlUnitTree = {}", pearlUnit.toStringTree());

		ProjectionRulesModel prModel = new ProjectionRulesModel();

		Tree firstChild = pearlUnit.getChild(0);

		Map<String, String> prefixMap = new HashMap<String, String>();

		if (firstChild.getType() == AntlrParser.AST_PROLOGUE) {
			parsePrologue(firstChild, prefixMap);
			parseRules(pearlUnit.getChild(1), prefixMap, prModel);
		} else {
			parseRules(firstChild, prefixMap, prModel);
		}

		return prModel;
	}

	private void parsePrologue(Tree prologue, Map<String, String> prefixMap) {
		logger.info("prologueTree = {}", prologue.toStringTree());

		for (int i = 0; i < prologue.getChildCount(); i++) {
			Tree child = prologue.getChild(i);

			parsePrefixDeclaration(child, prefixMap);
		}
	}

	private void parsePrefixDeclaration(Tree prefixDeclaration, Map<String, String> prefixMap) {
		logger.info("prefixDeclarationTree = {}", prefixDeclaration.toStringTree());

		String prefix = prefixDeclaration.getChild(0).getText();
		String namespace = prefixDeclaration.getChild(1).getText();

		prefixMap.put(prefix.substring(0, prefix.length() - 1),
				namespace.substring(1, namespace.length() - 1));

		logger.info("Prefix = {} Namespace = {}", prefix, namespace);
	}

	private void parseRules(Tree rules, Map<String, String> prefixMap, ProjectionRulesModel prModel) {
		logger.info("rulesTree = {}", rules.toStringTree());

		for (int i = 0; i < rules.getChildCount(); i++) {
			Tree child = rules.getChild(i);

			if (child.getType() == AntlrParser.AST_LAZY_RULE) {
				parseLazyRule(child, prefixMap, prModel);
			} else {
				parseRule(child, prefixMap, prModel);
			}
		}

	}

	private void parseRule(Tree rule, Map<String, String> prefixMap, ProjectionRulesModel prModel) {
		logger.info("ruleTree = {}", rule.toStringTree());

		ProjectionRule projectionRule = new ProjectionRule();
		projectionRule.setLaziness(false);

		for (int i = 0; i < rule.getChildCount(); i++) {
			Tree child = rule.getChild(i);

			switch (child.getType()) {
			case AntlrParser.AST_ID:
				parseId(child, projectionRule);
				break;
			case AntlrParser.AST_UIMA_TYPE_NAME:
				parseUimaTypeName(child, projectionRule);
				break;
			case AntlrParser.AST_BINDINGS_CLAUSE:
				parseBindingsClause(child, projectionRule);
				break;
			case AntlrParser.AST_NODES_CLAUSE:
				parseNodesClause(child, projectionRule, prefixMap);
				break;
			case AntlrParser.AST_GRAPH_CLAUSE:
				parseGraphClause(child, projectionRule, prefixMap);
				break;
			case AntlrParser.AST_WHERE_CLAUSE:
				parseWhereClause(child, projectionRule, prefixMap);
				break;
			default:
				logger.error("Unexpected node type {} inside a rule", child.getType());
				break;
			}
		}

		prModel.addProjectionRule(projectionRule);
	}

	private void parseLazyRule(Tree lazyRule, Map<String, String> prefixMap, ProjectionRulesModel prModel) {
		logger.info("lazyRuleTree = {}", lazyRule.toStringTree());

		ProjectionRule projectionRule = new ProjectionRule();
		projectionRule.setLaziness(true);

		for (int i = 0; i < lazyRule.getChildCount(); i++) {
			Tree child = lazyRule.getChild(i);

			switch (child.getType()) {
			case AntlrParser.AST_ID:
				parseId(child, projectionRule);
				break;
			case AntlrParser.AST_UIMA_TYPE_NAME:
				parseUimaTypeName(child, projectionRule);
				break;
			case AntlrParser.AST_NODES_CLAUSE:
				parseNodesClause(child, projectionRule, prefixMap);
				break;
			default:
				logger.error("Unexpected node type {} inside a rule", child.getType());
				break;
			}
		}

		prModel.addProjectionRule(projectionRule);
	}

	private void parseId(Tree tree, ProjectionRule projectionRule) {
		String ruleId = tree.getChild(0).getText().substring(3); // strip leading id:
		projectionRule.setRuleId(ruleId);
	}

	private void parseBindingsClause(Tree tree, ProjectionRule projectionRule) {
		for (int i = 0; i < tree.getChildCount(); i++) {
			Tree bindingDefTree = tree.getChild(i);

			String bindingName = bindingDefTree.getChild(0).getText();
			String featurePath = parseFeaturePath(bindingDefTree.getChild(1));
			String ruleId = bindingDefTree.getChild(2).getText().substring(3); // Strip leading id:

			BindingStruct bindingStruct = new BindingStruct(bindingName, featurePath, ruleId, projectionRule);
			projectionRule.addBinding(bindingStruct);
		}
	}

	private void parseNodesClause(Tree tree, ProjectionRule projectionRule, Map<String, String> prefixMap) {
		for (int i = 0; i < tree.getChildCount(); i++) {
			Tree nodeDefTree = tree.getChild(i);

			String name = nodeDefTree.getChild(0).getText();

			Tree typeAndConverterTree = nodeDefTree.getChild(1);

			String type = typeAndConverterTree.getChild(0).getText();

			int offset = 1;
			List<String> converterList;

			if (typeAndConverterTree.getChildCount() > offset && typeAndConverterTree.getChild(offset).getType() == AntlrParser.AST_CONVERTERS) {
				Tree converterListTree = typeAndConverterTree.getChild(offset);
				converterList = parseConverterList(converterListTree, prefixMap);
				offset++;
			} else {
				converterList = new ArrayList<String>(); // always assume the existence of a default converter
				converterList.add(DEFAULT_CONVERTER_URI);				
			}
						
			String lang = null;
			String datatype = null;
			
			if (typeAndConverterTree.getChildCount() > offset) {
				Tree placeholderLiteralAdditions = typeAndConverterTree.getChild(offset);
				if (placeholderLiteralAdditions.getType() == AntlrParser.AST_LANG) {
					lang = placeholderLiteralAdditions.getChild(0).getText().substring(1); // strip leading @
				} else if (placeholderLiteralAdditions.getType() == AntlrParser.AST_DATATYPE) {
					datatype = parseIri(placeholderLiteralAdditions.getChild(0), prefixMap);
				}
				offset++;
			}

			String featurePath = parseFeaturePath(nodeDefTree.getChild(2));
			
			PlaceholderStruct placeholder = new PlaceholderStruct(name, type, converterList, featurePath,
					projectionRule);
			if (lang != null) {
				placeholder.setLiteralLang(lang);
			} else if (datatype != null) {
				placeholder.setLiteralDatatype(datatype);
			}
			projectionRule.addPlaceholder(placeholder);
		}
	}

	private List<String> parseConverterList(Tree converterListTree, Map<String, String> prefixMap) {
		List<String> converterList = new ArrayList<String>(converterListTree.getChildCount());
		for (int i = 0; i < converterListTree.getChildCount(); i++) {
			Tree iriTree = converterListTree.getChild(i);
			String iri = parseIri(iriTree, prefixMap);
			converterList.add(iri);
		}
		return converterList;
	}

	private String parseIri(Tree iriTree, Map<String, String> prefixMap) {
		switch (iriTree.getType()) {
		case AntlrParser.AST_IRI_REF:
			return parseIriRef(iriTree);
		case AntlrParser.AST_PREFIXED_NAME:
			return parsePrefixedName(iriTree, prefixMap);
		}

		throw new IllegalArgumentException("This should never happen. Unknown node type: "
				+ iriTree.getType());
	}

	private String parseIriRef(Tree iriTree) {
		return iriTree.getChild(0).getText().substring(1, iriTree.getChild(0).getText().length() - 1);
	}

	private String parsePrefixedName(Tree iriTree, Map<String, String> prefixMap) {
		String qname = iriTree.getChild(0).getText();
		String[] parts = qname.split(":");

		return prefixMap.get(parts[0]) + parts[1];
	}

	private void parseGraphClause(Tree tree, ProjectionRule projectionRule, Map<String, String> prefixMap) {
		Collection<GraphElement> graphElementList = parseGraph(tree.getChild(0), prefixMap, projectionRule);
		projectionRule.addElementsToGraph(graphElementList);
	}

	private void parseWhereClause(Tree tree, ProjectionRule projectionRule, Map<String, String> prefixMap) {
		Collection<GraphElement> whereElementList = parseGraph(tree.getChild(0), prefixMap, projectionRule);
		projectionRule.addElementsToWhere(whereElementList);
	}

	private Collection<GraphElement> parseGraph(Tree tree, Map<String, String> prefixMap,
			ProjectionRule projectionRule) {
		List<GraphElement> graphElements = new ArrayList<GraphElement>();
		for (int i = 0; i < tree.getChildCount(); i++) {
			Tree graphElementTree = tree.getChild(i);

			GraphElement ge;
			if (graphElementTree.getType() == AntlrParser.AST_GRAPH_TRIPLE) {
				ge = parseGraphTriple(graphElementTree, prefixMap, projectionRule);
			} else { // graphElementTree.getType() == AntlrParser.AST_OPTIONAL
				ge = parseOptionalGraphElement(graphElementTree, prefixMap, projectionRule);
			}

			graphElements.add(ge);
		}

		return graphElements;
	}

	private GraphElement parseOptionalGraphElement(Tree tree, Map<String, String> prefixMap,
			ProjectionRule projectionRule) {
		List<GraphStruct> graphElements = new ArrayList<GraphStruct>();
		for (int i = 0; i < tree.getChildCount(); i++) {
			Tree graphElementTree = tree.getChild(i);

			GraphStruct ge = parseGraphTriple(graphElementTree, prefixMap, projectionRule);

			graphElements.add(ge);
		}

		return new OptionalGraphStruct(graphElements, projectionRule);
	}

	private GraphStruct parseGraphTriple(Tree graphElementTree, Map<String, String> prefixMap,
			ProjectionRule projectionRule) {
		Tree subjectTree = graphElementTree.getChild(0);
		GraphSingleElement subj = parseGraphSingleElement(subjectTree, prefixMap, projectionRule);

		Tree predicateTree = graphElementTree.getChild(1);
		GraphSingleElement pred = parseGraphSingleElement(predicateTree, prefixMap, projectionRule);

		Tree objectTree = graphElementTree.getChild(2);
		GraphSingleElement obj = parseGraphSingleElement(objectTree, prefixMap, projectionRule);

		return new GraphStruct(subj, pred, obj, projectionRule);
	}

	private GraphSingleElement parseGraphSingleElement(Tree tree, Map<String, String> prefixMap,
			ProjectionRule projectionRule) {
		switch (tree.getType()) {
		case AntlrParser.AST_IRI_REF:
			return new GraphSingleElemUri(parseIriRef(tree), projectionRule);
		case AntlrParser.AST_PREFIXED_NAME:
			return new GraphSingleElemUri(parsePrefixedName(tree, prefixMap), projectionRule);
		case AntlrParser.AST_BLANK_NODE:
			return new GraphSingleElemBNode(parseBlankNode(tree), projectionRule);
		case AntlrParser.AST_PLACE_HOLDER:
			return new GraphSingleElemPlaceholder(parsePlaceholder(tree), projectionRule);
		case AntlrParser.AST_VAR:
			return new GraphSingleElemVar(parseVariable(tree), projectionRule);
		case AntlrParser.AST_ABBR:
			return new GraphSingleElemUri(resolveAbbreviation(tree.getChild(0).getText()), projectionRule);
		}

		throw new IllegalArgumentException("Unknownn graph element type: " + tree.getType());
	}

	private String resolveAbbreviation(String text) {
		if (text.equals("a")) {
			return "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
		}

		throw new IllegalArgumentException("Unknown abbreviation");
	}

	private String parseVariable(Tree tree) {
		return tree.getChild(0).getText();
	}

	private String parsePlaceholder(Tree tree) {
		if (tree.getChildCount() == 2) {
			return (tree.getChild(0).getText() + "." + tree.getChild(1).getText());
		} else {
			return tree.getChild(0).getText();
		}
	}

	private String parseBlankNode(Tree tree) {
		return tree.getChild(0).getText().substring(2); // strip leading _:
	}

	private void parseUimaTypeName(Tree tree, ProjectionRule projectionRule) {
		Tree javaNameTree = tree.getChild(0);
		StringBuilder sb = new StringBuilder();

		for (int i = 0; i < javaNameTree.getChildCount(); i++) {
			if (i != 0) {
				sb.append(".");
			}

			sb.append(javaNameTree.getChild(i).getText());
		}

		projectionRule.setUimaTypeName(sb.toString());
	}

	private String parseFeaturePath(Tree tree) {
		StringBuilder sb = new StringBuilder();

		for (int i = 0; i < tree.getChildCount(); i++) {
			if (i != 0) {
				sb.append("/");
			}

			Tree pathElementTree = tree.getChild(i);
			sb.append(pathElementTree.getChild(0).getText());

			if (pathElementTree.getChildCount() == 2) {
				sb.append("[" + pathElementTree.getChild(1).getText() + "]");
			}
		}

		return sb.toString();
	}

}
